﻿/* Autoduel Game
 * CS4911 Senior Design Project
 * Fall 2010 Semester
 * Ian Axelrod, Liz Ha, Andrew Muldowney, Edward Futch
 */

using System;
using System.Collections;
using System.Windows.Forms;

using AutoduelClient.GameClient;
using AutoduelClient.GameClient.Network;
using AutoduelClient.GameClient.Player;
using AutoduelClient.GameClient.World;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using AutoduelClient.GameClient.Locations;
using System.Threading;

namespace AutoduelClient.GameUI
{
    /// <summary>
    /// This class deals with rendering and logic of the entire game
    /// </summary>
    public class Game : Microsoft.Xna.Framework.Game
    {
		#region Static Fields (4)
        // try connecting every 1 second
        private const int DELAY = 250;
        // how many times 
        private const int NUM_DELAYS = 4;
		// vector used for movement deltas
        private const int MV_DX = 3;
		private const int MV_DY = 3;
		// defined size of our message queue
        private const int QUEUE_SIZE = 32;
        // frames per second the game is to run at
        private const float FPS = 30.0f;

		// special username that reveals administrative options
        private const string USERNAME_ADMIN = "ADMIN";
		#endregion

		#region Fields (12)
		// client used for server communications
        private Client client;

		// link to the screen holding the XNA frame
        private Screen screen;

        /// <summary>
        /// Get/Set the link to the screen holding the XNA frame
        /// </summary>
        public Screen Screen
        {
            get { return screen; }
            set { screen = value; }
        }

        // the form that caused us to disable ourselves
        private Form disablerForm;

		// performs primitive-based rendering, creates resources, handles system-level variables, 
        // adjusts gamma ramp levels, and creates shaders
        private GraphicsDevice device;

		// enables a group of sprites to be drawn using the same settings
        private SpriteBatch spriteBatch;

		// the font for playerNames
        private SpriteFont fontChoice;

		// a two-dimensional texture used to represent the grid background
        private Texture2D backgroundTexture;
		// the player texture
        private Texture2D playerTexture;

		// handles the configuration and management of the graphics device
        private GraphicsDeviceManager graphics;

		// A message queue that is filled by the client's interactions with the server and
        // emptied by XNA
        private Queue messages = Queue.Synchronized(new Queue(QUEUE_SIZE));

		// the dimensions of the screen
        private int screenWidth, screenHeight;

		// the draw surface
        private IntPtr drawSurface;

		#endregion

		#region Properties (5)
		/// <summary>
        /// Gets/Sets the client used for server communications
        /// </summary>
        public Client Client
        {
            get { return client; }
            set { client = value; }
        }
		/// <summary>
        /// Gets/Sets the message queue that is filled by the client's interactions
        /// with the server and emptied by XNA
        /// </summary>
        public Queue Messages
        {
            get { return messages; }
            set { messages = value; }
        }
		/// <summary>
        /// Gets/Sets the player texture
        /// </summary>
        public Texture2D PlayerTexture
        {
            get { return playerTexture; }
            set { playerTexture = value; }
        }
		/// <summary>
        /// Get/Set the screen height
        /// </summary>
        public int ScreenHeight
        {
            get { return screenHeight; }
            set { screenHeight = value; }
        }
		/// <summary>
        /// Get/Set the screen width
        /// </summary>
        public int ScreenWidth
        {
            get { return screenWidth; }
            set { screenWidth = value; }
        }
		#endregion

		#region Constructors (1)
		/// <summary>
        /// Construct the Game client
        /// </summary>
        /// <param name="drawSurface">Pointer to the surface to draw on</param>
        public Game(IntPtr drawSurface, Screen screen)
        {
            this.graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            this.drawSurface = drawSurface;
            this.graphics.PreparingDeviceSettings +=
                new EventHandler<PreparingDeviceSettingsEventArgs>(Graphics_PreparingDeviceSettings);
            Control.FromHandle(Window.Handle).VisibleChanged +=
              new EventHandler(Game_VisibleChanged);
            this.screen = screen;
        }
		#endregion

		#region Methods (8)

		/// <summary>
        /// Called after all components are initialized but before the first update in the game loop.
        /// </summary>
        protected override void BeginRun()
        {
            this.TargetElapsedTime = TimeSpan.FromSeconds(1.0f / FPS);
            //this.IsFixedTimeStep = false;
            this.client.SendMessage(new byte[] { (byte)OPCODES.REQ_UPDWORLD });
        }

		/// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            
            /*System.Diagnostics.Debug.WriteLine("Grid[0,0].RC: " + this.client.World.Grids[0, 0].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[0,1].RC: " + this.client.World.Grids[0, 1].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[0,2].RC: " + this.client.World.Grids[0, 2].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[1,0].RC: " + this.client.World.Grids[1, 0].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[1,1].RC: " + this.client.World.Grids[1, 1].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[1,2].RC: " + this.client.World.Grids[1, 2].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[2,0].RC: " + this.client.World.Grids[2, 0].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[2,1].RC: " + this.client.World.Grids[2, 1].RealCoords.ToString());
            System.Diagnostics.Debug.WriteLine("Grid[2,2].RC: " + this.client.World.Grids[2, 2].RealCoords.ToString());*/
            
            // determine where the player is in relation to the center of the middle grid
            Vector2 playerShift = new Vector2(0, 0);
            Vector2 gridShift = new Vector2(0, 0);

            if (this.client.World.LocalPlayer != null)
            {
                playerShift.X = this.screenWidth / 2 - this.client.World.LocalPlayer.CurrentCoord.X;
                playerShift.Y = this.screenHeight / 2 - this.client.World.LocalPlayer.CurrentCoord.Y;
                gridShift.X = this.client.World.LocalPlayer.CurrentGrid.RealCoords.X + this.screenWidth / 2 - this.client.World.LocalPlayer.CurrentCoord.X;
                gridShift.Y = this.client.World.LocalPlayer.CurrentGrid.RealCoords.Y + this.screenHeight / 2 - this.client.World.LocalPlayer.CurrentCoord.Y;
            }

            GraphicsDevice.Clear(Color.Chocolate);

            this.spriteBatch.Begin();

            // draw background for the grid
            this.spriteBatch.Draw(this.backgroundTexture, new Rectangle(0, 0, this.screenWidth, this.screenHeight), Color.White);

            // draw all the 9 nine grids, alter their positions based upon where the player is from the center of the middle grid
            int x = -1; int y = -1;

            /*System.Diagnostics.Debug.WriteLine("playerShift: " + playerShift.ToString());
            System.Diagnostics.Debug.WriteLine("GridShift: " + gridShift.ToString());
            System.Diagnostics.Debug.WriteLine(this.client.World.grids[1, 1].RealCoords);*/
            foreach (Grid grid in this.client.World.Grids)
            {
                if (grid.BgResource != null)
                {

                    this.spriteBatch.Draw(grid.BgResource, new Rectangle((int)((this.screenWidth * x) + gridShift.X), (int)((this.screenHeight * y) + gridShift.Y), this.screenWidth, this.screenHeight), Color.White);
                }
                x++;
                if (x == 2) { x = -1; y++; }
            }

            // draw all the players in the local world
            foreach (Player player in this.client.World.Players.Values)
            {
                if (player != this.client.World.LocalPlayer)
                {
                    if (!player.InLocation)
                    {
                        player.PlayerSprite.Draw(this.spriteBatch, (int)(playerShift.X), (int)(playerShift.Y), false);
                        spriteBatch.DrawString(this.fontChoice, player.Name.Substring(9), new Vector2(player.CurrentCoord.X + 5 + playerShift.X, player.CurrentCoord.Y + 10 + playerShift.Y), Color.White);
                    }
                }
                else
                {
                    if (!this.client.World.LocalPlayer.InLocation)
                    {
                        player.PlayerSprite.Draw(this.spriteBatch, 0, 0, false);
                        spriteBatch.DrawString(this.fontChoice, player.Name.Substring(9), new Vector2(this.screenWidth / 2, this.screenHeight / 2), Color.White);
                    }
                }
            }

            this.spriteBatch.End();

            base.Draw(gameTime);
        }

		/// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            this.graphics.PreferredBackBufferWidth = this.screen.DrawSurfaceDim.X;
            this.graphics.PreferredBackBufferHeight = this.screen.DrawSurfaceDim.Y;
            this.graphics.IsFullScreen = false;
            this.graphics.ApplyChanges();

            base.Initialize();
        }

		/// <summary>
        /// LoadContent will be called once per game and is the method that loads
        /// all of the game content (ie: textures and objects).
        /// </summary>
        protected override void LoadContent()
        {
            this.device = graphics.GraphicsDevice;
            this.spriteBatch = new SpriteBatch(this.device);
            this.fontChoice = Content.Load<SpriteFont>("playerNames");

            // set screen dimensions
            this.screenWidth = this.device.PresentationParameters.BackBufferWidth;
            this.screenHeight = this.device.PresentationParameters.BackBufferHeight;

            // background Image code
            this.backgroundTexture = Content.Load<Texture2D>(@"data\grid\green-bg");

            // player texture
            this.playerTexture = Content.Load<Texture2D>(@"data\player\player");
        }

		/// <summary>
        /// Connects service layer to presentation layer, allowing messages
        /// to be displayed to the user
        /// NOTE: This version does not allow the game to receive any more messages!
        /// Use only when planning to terminate the game after the message
        /// </summary>
        /// <param name="message">The message to display</param>
        /// <param name="caption">The caption for the message box</param>
        /// <param name="buttons">The buttons for the message box</param>
        /// <param name="icon">The icon for the message box</param>
        /// <returns>The value of the button clicked in the message</returns>
        public DialogResult ShowMessageBlocking(string message, string caption, System.Windows.Forms.MessageBoxButtons buttons, System.Windows.Forms.MessageBoxIcon icon)
        {
            return this.screen.ShowMessageBox(message, caption, buttons, icon);
        }

		/// <summary>
        /// Inital elements that need to be activated and set up
        /// </summary>
        public void Start()
        {
            this.client = new Client(this);
            this.client.World.Game = this;

            // present the login screen to the user
            Login login = new Login();

            if (login.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                this.client.LoginUser(login.UsernameTxt.Text, login.HostnameText.Text);
                
                int i = 0;

                while (!this.client.IsLoggedIn && i < NUM_DELAYS) 
                { 
                    Thread.Sleep(DELAY); 
                    i++;  
                }
                
                // if we are an admin, we need to get approval
                // from the server before we can enable our administrative
                // funcionality on the client side
                if (login.UsernameTxt.Text == USERNAME_ADMIN)
                {
                    this.client.SendMessage(new byte[] { (byte)OPCODES.REQ_ADMIN });
                }
            }
            else
            {
                return;
            }

            login.Dispose();

            // show the game screen
            this.screen.Game = this;
            this.screen.Show();
            this.screen.Text = this.screen.Text+": "+login.UsernameTxt.Text;

            // starts XNA screen embedded in the game screen
            this.Run();
        }

		/// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // check if we are attempting to exit the game
            if (Keyboard.GetState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape))
            {
                this.Exit();
            }

            // parse and react on queued messages from the server
            while (Messages.Count > 0)
            {
                Packet message = (Packet)Messages.Dequeue();

                    switch (message.Message[0])
                    {
                        case (byte)OPCODES.ORD_UPDPLAYER:
                            {
                                Player tmpPlayer = Player.DecodePacket(message.Message);
                                Player aPlayer = this.client.World.GetPlayerByName(tmpPlayer.Name);

                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_UPDPLAYER message");

                                // player exists, so let's update his attributes
                                if (aPlayer != null)
                                {
                                    aPlayer.Name = tmpPlayer.Name;
                                    aPlayer.MechanicSkill = tmpPlayer.MechanicSkill;
                                    aPlayer.Health = tmpPlayer.Health;
                                    aPlayer.CurrentCoord = tmpPlayer.CurrentCoord;
                                    aPlayer.InLocation = tmpPlayer.InLocation;

                                    // determine if we need to switch the grid this player
                                    // is a denizen of
                                    Grid grid = this.client.World.DetermineGridFromCoordsRelative(
                                            tmpPlayer.CurrentCoord);
                                    if (aPlayer.CurrentGrid.RealCoords != grid.RealCoords)
                                    {
                                        aPlayer.CurrentGrid.RemovePlayer(aPlayer);
                                        aPlayer.CurrentGrid = grid;
                                        aPlayer.CurrentGrid.AddPlayer(aPlayer);
                                    }
                                }
                                // player doesn't exist, so we need to add him to the world & grid
                                else
                                {
                                    if (this.client.Player == null && tmpPlayer.Name == Client.PLAYER_BIND_PREFIX + this.client.Username)
                                    {
                                        this.client.Player = tmpPlayer;
                                        this.client.Player.Client = this.client.SimpleClient;
                                        this.client.Player.Game = this;
                                        this.client.World.LocalPlayer = tmpPlayer;
                                        this.client.Player.LocationFound += new LocationFoundEventHandler(QueryLocation);
                                    }
                                    tmpPlayer.PlayerSprite = new AnimatedSprite(PlayerTexture,
                                                0, 0, 36, 64, 16);
                                    tmpPlayer.CurrentGrid = this.client.World.DetermineGridFromCoordsRelative(
                                            tmpPlayer.CurrentCoord);
                                    tmpPlayer.CurrentGrid.AddPlayer(tmpPlayer);
                                    this.client.World.AddPlayer(tmpPlayer);
                                }

                                System.Diagnostics.Debug.WriteLine("[Client] Player updated to: " + tmpPlayer.ToString());
                                break;
                            }
                        case (byte)OPCODES.ORD_UPDGRID:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_UPDGRID:");
                                Grid tmpGrid = Grid.DecodePacket(message.Message);

                                switch (tmpGrid.Background)
                                {
                                    case "Dessert":
                                        {
                                            System.Diagnostics.Debug.WriteLine("LOADED DESSERT!");
                                            tmpGrid.BgResource = this.Content.Load<Texture2D>(@"data\grid\bgroad-desert");
                                            break;
                                        }
                                    case "Town":
                                        {
                                            System.Diagnostics.Debug.WriteLine("LOADED TOWN!");
                                            tmpGrid.BgResource = this.Content.Load<Texture2D>(@"data\grid\bgtown-main");
                                            break;
                                        }
                                    case "Trees":
                                        {
                                            System.Diagnostics.Debug.WriteLine("LOADED TREES!");
                                            tmpGrid.BgResource = this.Content.Load<Texture2D>(@"data\grid\bgroad-trees");
                                            break;
                                        }
                                    default:
                                        {
                                            System.Diagnostics.Debug.WriteLine("Attempted to load unknown background");
                                            break;
                                        }
                                }
                                this.client.World.UpdateGrid(tmpGrid);
                                System.Diagnostics.Debug.WriteLine("[Client] World grid updated with " + tmpGrid);
                                break;
                            }
                        case (byte)OPCODES.ORD_ADMIN:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_ADMIN message");
                                // make the admin tool menu available
                                screen.MainMenuStrip.Items[1].Visible = true;
                                break;
                            }
                        case (byte)OPCODES.ORD_UPDPLAYERLIST:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_UPDPLAYERLIST message");
                                Player player = Player.DecodePacket(message.Message);

                                this.client.World.ServerPlayers.Add(player.Name, player);
                                break;
                            }
                        case (byte)OPCODES.ORD_ENDUPDPLAYERLIST:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_ENDUPDPLAYERLIST message");
                                this.client.World.ServerPlayersReady = true;
                                break;
                            }
                        case (byte)OPCODES.ORD_UPDLOC:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_UPDLOC message");
                                Location location = Location.DecodePacket(message.Message);

                                if (location is TruckStop)
                                {
                                    ((TruckStop)location).ChassisListRequested += DisplayChassisList;
                                    ((TruckStop)location).RumorsListRequested += DisplayRumorsList;
                                }

                                Grid grid = this.Client.World.Grids[1, 1];

                                if (grid != null) {
                                    System.Diagnostics.Debug.WriteLine("[Client] Location added to grid: " + grid);
                                    try
                                    {
                                        grid.Locations.Add(location.Entrance, location);
                                    }
                                    catch (ArgumentException) { }
                                }
                                break;
                            }
                        case (byte)OPCODES.ORD_UPDCHASSESLIST:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_UPDCHASSESLIST message");
                                Chassis chassis = Chassis.DecodePacket(message.Message);

                                TruckStop.Chasses.Add(chassis);
                                break;
                            }
                        case (byte)OPCODES.ORD_REMPLAYER:
                            {
                                System.Diagnostics.Debug.WriteLine("[Client] Dequeued ORD_REMPLAYER message");
                                this.client.World.RemovePlayer(this.client.World.GetPlayerByName((Player.DecodePacket(message.Message)).Name));
                                break;
                            }
                        default:
                            break;
                    }
                }

            // get user input
            UpdateInput();

            // update various player information drawn on screen
            foreach (Player player in this.client.World.Players.Values)
            {
                // update sprite position
                if (player != this.client.World.LocalPlayer)
                {
                    player.PlayerSprite.X = player.CurrentCoord.X;
                    player.PlayerSprite.Y = player.CurrentCoord.Y;
                }
            }



            // update various local player information drawn on screen
            if (this.client.World.LocalPlayer != null)
            {
                // update sprite position
                this.client.World.LocalPlayer.PlayerSprite.X = this.screenWidth / 2;
                this.client.World.LocalPlayer.PlayerSprite.Y = this.screenHeight / 2;

                // update health
                this.screen.SetHealth(this.client.World.LocalPlayer.Health);
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Callback method called a location is found
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void QueryLocation(object sender, LocationFoundEventArgs e)
        {
            this.screen.Enabled = false;
            this.disablerForm = new AsyncMessageBox(EnterLocation, "Do you want to enter the '" + e.location.Name + "'?",
                "You have come near a location!", System.Windows.Forms.MessageBoxButtons.YesNo,
                System.Windows.Forms.MessageBoxIcon.Question);
            this.disablerForm.Show(this.screen);
        }

        private void DisplayChassisList(object sender, ChassisListEventArgs e)
        {
            this.disablerForm.Enabled = false;
            new ChassesDialog(this.client.Player, e.chassis).Show(this.disablerForm);
        }

        private void DisplayRumorsList(object sender, RumorsListEventArgs e)
        {
            string msg = "";

            foreach (object str in e.rumors)
            {
                msg = msg + str + '\n';
            }
            this.disablerForm.Enabled = false;
            new AsyncMessageBox(msg, "Rumors",
                MessageBoxButtons.OK, MessageBoxIcon.Information).Show(this.disablerForm);
        }

        /// <summary>
        /// Callback method from AsyncMessageBox
        /// Called when a button is clicked
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void EnterLocation(object sender, ButtonClickEventArgs e)
        {
            switch (e.dialogResult)
            {
                case DialogResult.Yes:
                    {
                        this.client.Player.NearestLocation.Enter(this.client.Player);
                        this.screen.Enabled = false;
                        this.disablerForm = (new LocationOptionsDialog(this.client.Player.CurrentLocation));
                        this.disablerForm.Show(this.screen);
                        break;
                    }
                default:
                    {
                        this.client.Player.Move(new Coordinate(-50, 0));
                        break;
                    }
            }
        }

		/// <summary>
        /// Gets user input and performs actions associated with that input
        /// </summary>
        private void UpdateInput()
        {
            if (this.screen.IsEntered && this.client.World.LocalPlayer != null && this.client.World.LocalPlayer.CurrentLocation == null)
            {
                KeyboardState currentKeyState = Keyboard.GetState();

                // create movement vector
                int dx = 0, dy = 0;

                if (currentKeyState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Right))
                {
                    dx = MV_DX;
                }
                else if (currentKeyState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Left))
                {
                    dx = -MV_DX;
                }

                if (currentKeyState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Down))
                {
                    dy = MV_DY;
                }
                else if (currentKeyState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Up))
                {
                    dy = -MV_DY;
                }

                // move the player
                if (this.client.World.LocalPlayer != null)
                {
                    if (dx != 0 || dy != 0)
                    {
                        this.client.World.LocalPlayer.Move(new Coordinate(dx, dy));
                    }
                    // TODO: this needs to take in the player speed from the player object
                    // not manually from the input
                    this.screen.SetSpeed(Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)));
                }
                else
                {
                    this.screen.SetSpeed(0);
                }
            }
            else
            {
                this.screen.SetSpeed(0);
            }
        }

		#endregion

		#region Event Handlers (2)

		/// <summary>
        /// Occurs when the original gamewindows' visibility changes and makes sure it stays invisible
        /// </summary>
        ///<param name="sender"></param>
        ///<param name="e"></param>
        private void Game_VisibleChanged(object sender, EventArgs e)
        {
            if (Control.FromHandle(Window.Handle).Visible == true)
                Control.FromHandle(Window.Handle).Visible = false;
        }

		/// <summary>
        /// Event capturing the construction of a draw surface and makes sure this gets redirected to
        /// a predesignated drawsurface marked by pointer drawSurface
        /// </summary>
        ///<param name="sender"></param>
        ///<param name="e"></param>
        void Graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = drawSurface;
        }

		#endregion
    }
}
